package com.google.android.daca.service;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import oauth.signpost.OAuth;
import oauth.signpost.OAuthConsumer;
import oauth.signpost.OAuthProvider;
import oauth.signpost.commonshttp.CommonsHttpOAuthConsumer;
import oauth.signpost.commonshttp.CommonsHttpOAuthProvider;
import oauth.signpost.exception.OAuthException;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import android.util.Log;

import com.google.android.daca.exceptions.AuthenticationErrorException;
import com.google.android.daca.exceptions.BadRequestException;
import com.google.android.daca.exceptions.ColumnConstraintExceededException;
import com.google.android.daca.exceptions.InvalidResponseCodeException;
import com.google.android.daca.exceptions.ParticipantAlreadyExistsException;
import com.google.android.daca.exceptions.ParticipantIDNotReturnedException;
import com.google.android.daca.exceptions.RowConstraintExceededException;
import com.google.android.daca.types.CreatePollResponse;
import com.google.android.daca.types.Participant;
import com.google.android.daca.types.Poll;
import com.google.android.daca.util.XMLUtil;
/**
 * This class is acting as a client proxy for Doodle RESTful services. It takes care of authentication
 * and authorization, as well as communication through HTTP protocol with the backend.
 * 
 * @author pavel
 *
 */
public class DoodleRESTService {

	private HttpClient client;
	private OAuthConsumer oauthConsumer;
	private OAuthProvider oauthProvider;
	private DocumentBuilder builder;
	/**
	 * Initializes new Service with defaults
	 */
	public DoodleRESTService() {
		client = new DefaultHttpClient();
		oauthConsumer = new CommonsHttpOAuthConsumer(
				"xqz1uangnkueo1l4fka1kxx7pwuhs1qq",
				"n9objzq51vlhfoq25pgyblugwrbad25e"
		);
		oauthProvider = new CommonsHttpOAuthProvider(
				"http://doodle.com/api1/oauth/requesttoken",
				"http://doodle.com/api1/oauth/accesstoken",
		"http://doodle.com/api1/oauth/authorizeConsumer");
		try {
			builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
		} catch (ParserConfigurationException pcEx) {
			throw new RuntimeException("Cannot create document builder", pcEx);
		}
	}
	/**
	 * This method retrieves a poll from doodle server given it's 16 characters identifier
	 * @param id
	 * @return Java representation of the poll
	 * @throws Exception In the following cases exception is thrown<list><li>When a poll with the passed identifier does not exist or is deleted</li><li>When the client cannot be authenticated with the server</li>
	 * <li>When the response from the server cannot be parsed</li><li>When communication error occurred</li>
	 * </list>
	 */
	public Poll getPollByID(String id) throws Exception{
		final String TAG = "DoodleRESTService#getPollByID";
		try {
			//make oauth handshake to authorize consumer
			makeOAuth2LeggedHandshake();
			//create new GET request
			HttpGet request = new HttpGet("http://doodle.com/api1/polls/" + id);
			//sign the request
			oauthConsumer.sign(request);
			//execute the request
			HttpResponse response = client.execute(request);
			StatusLine status = response.getStatusLine();
			switch (status.getStatusCode()) {
			case HttpStatus.SC_OK: break;
			case HttpStatus.SC_NOT_FOUND: throw new Exception("no such poll");
			case HttpStatus.SC_GONE: throw new Exception("the poll has been deleted");
			default: {
				handleUnexpectedResponse(TAG, response);
				throw new Exception("Invalid response code: " + status.toString());}
			}
			HttpEntity entity = response.getEntity();
			//parse response body
			Document rootDoc = builder.parse(entity.getContent());
			if (rootDoc == null) return null;
			//construct Poll object from the parsed document
			Poll result = XMLUtil.getPollFromDocument(rootDoc);
			return result;
		} catch (OAuthException oaEx) {
			throw new Exception("Authentication error occurred", oaEx);
		} catch (SAXException sEx) {
			throw new Exception("Error while parsing the response message", sEx);
		} catch (IOException ioEx) {
			throw new Exception("Communication error occurred", ioEx);
		}
	}

	/**
	 * This method allows the user to vote in a poll.
	 * @param participant Information about the participant and his/her preferences.
	 * Participant's name must not be empty
	 * @param pollID 16-character identifier of the poll.
	 * @return {@link String} Participant's identifier
	 * @throws Exception in the following cases exception is thrown
	 * <list>
	 * <li>When the name of the participant is empty</li>
	 * <li>When the name of the participant is already in use or a constraint was violated by this vote</li>
	 * <li>When the client cannot be authenticated with the server</li>
	 * <li>When the server responds with unexpected response code</li>
	 * <li>When the server does not return participant identifier</li>
	 * <li>When communication error occurred</li>
	 * </list>
	 */
	public String participate(Participant participant, String pollID)
	    throws ParticipantAlreadyExistsException, RowConstraintExceededException,
	           ColumnConstraintExceededException, ParticipantIDNotReturnedException,
	           InvalidResponseCodeException, AuthenticationErrorException, BadRequestException,
	           IOException
	{
		final String TAG = "DoodleRESTService#particpate";
		try 
		{
			makeOAuth2LeggedHandshake();
			//create new POST request
			HttpPost request = new HttpPost("http://doodle.com/api1/polls/" + pollID + "/participants");
			//construct the xml payload
			String payload = XMLUtil.serializeParticipant(participant);
			request.setEntity(new StringEntity(payload));
			request.setHeader("Content-Type", "application/xml");
			//sign the request
			oauthConsumer.sign(request);
			//execute the request
			HttpResponse response = client.execute(request);
			StatusLine status = response.getStatusLine();
			//we expect 201 Created
			switch (status.getStatusCode()) 
			{
			  case HttpStatus.SC_CREATED: break;
			  case HttpStatus.SC_BAD_REQUEST: throw new BadRequestException("Name is empty.");
			  case HttpStatus.SC_CONFLICT: 
			  {
				HttpEntity entity = response.getEntity();
				if (entity != null)
				{
					BufferedReader reader = new BufferedReader(new InputStreamReader(entity.getContent()));
					StringBuilder builder = new StringBuilder();
					String line;
					while ((line = reader.readLine()) != null)
					{
						builder.append(line).append(System.getProperty("line.separator"));
					}
					Log.e(TAG, builder.toString());
					if (builder.indexOf("participant with name") >= 0) 
					{
						throw new ParticipantAlreadyExistsException("Participant with name " + participant.getName() + " already exists.");
					} 
					else if (builder.indexOf("row constraint") >= 0) 
					{
						throw new RowConstraintExceededException("Row constraint exceeded. You may select only one option.");
					}
					else 
					{
						throw new ColumnConstraintExceededException("Column constraint exceeded.");
					}
				}
			}
			  //unexpected response code
			  default: throw new InvalidResponseCodeException("Invalid response code", status.getStatusCode());
			}
			Header[] responseHeaders = response.getHeaders("Content-Location");
			if (responseHeaders == null || responseHeaders.length == 0)
			{
				Log.e(TAG, Arrays.deepToString(response.getAllHeaders()));
				throw new ParticipantIDNotReturnedException("Participant ID was not returned by server.");
			}
			return responseHeaders[0].getValue();
		} 
		catch (OAuthException oaEx) 
		{
			throw new AuthenticationErrorException("Authentication error occurred.", oaEx);
		}
	}
	/**
	 * Creates a new poll using Doodle RESTful API
	 * @param poll The poll which will be created. Mandatory fields are: title, description (could be empty string),
	 * hidden, levels, type, initiator (name is required) and options
	 * @return The 16-character Poll identifier and 8-character administrator key, wrapped in {@link CreatePollResponse} object
	 * @throws Exception in the following cases exception is thrown
	 * <list>
	 * <li>When one of the required properties of a {@link Poll} is not set</li>
	 * <li>When the client cannot be authenticated with the server</li>
	 * <li>When the server responds with unexpected response code</li>
	 * <li>When the server does not return poll identifier</li>
	 * <li>When communication error occurred</li>
	 * </list>
	 */
	public CreatePollResponse createPoll(Poll poll) throws Exception{
		final String TAG = "DoodleRESTService#createPoll";
		try {
			makeOAuth2LeggedHandshake();
			//create new POST request
			HttpPost request = new HttpPost("http://doodle.com/api1/polls/");
			//construct the xml payload
			String payload = XMLUtil.serializePoll(poll);
			request.setEntity(new StringEntity(payload));
			request.setHeader("Content-Type", "application/xml");
			//sign the request
			oauthConsumer.sign(request);
			//execute the request
			HttpResponse response = client.execute(request);
			StatusLine status = response.getStatusLine();
			//we expect 201 Created
			switch (status.getStatusCode()) {
			case HttpStatus.SC_CREATED: break;
			//unexpected response code
			default: {
				handleUnexpectedResponse(TAG, response);
				throw new Exception("Invalid response code: " + status.toString());}
			}
			Header contentLocationHeader = response.getFirstHeader("Content-Location");
			if (contentLocationHeader == null) {
				Log.e(TAG, Arrays.deepToString(response.getAllHeaders()));
				throw new Exception("Poll ID was not returned by server");
			}
			String pollID =  contentLocationHeader.getValue();
			Header xDoodleKeyHeader = response.getFirstHeader("X-DoodleKey");
			if (xDoodleKeyHeader == null) {
				Log.e(TAG, Arrays.deepToString(response.getAllHeaders()));
				throw new Exception("8-character DoodleKey was not returned by server");
			}
			String adminKey = xDoodleKeyHeader.getValue();
			return new CreatePollResponse(pollID, adminKey);
		} catch (OAuthException oaEx) {
			throw new Exception("Authentication error occurred", oaEx);
		} catch (IOException ioEx) {
			throw new Exception("Communication error occurred", ioEx);
		}
	}
	/**
	 * Edits a poll. Valid changes include - closing the poll, changing the poll title, etc.
	 * @param poll The new Java representation of the poll
	 * @param pollID 16-characters poll identifier
	 * @param adminKey 8-characters administrator key
	 * @throws Exception in the following cases exception is thrown
	 * <list>
	 * <li>When the administrator key is incorrect</li>
	 * <li>When the name of the participant is already in use or a constraint was violated by this vote</li>
	 * <li>When the client cannot be authenticated with the server</li>
	 * <li>When the server responds with unexpected response code</li>
	 * <li>When the server does not return participant identifier</li>
	 * <li>When communication error occurred</li>
	 * </list>
	 */
	public void editPoll(Poll poll, String pollID, String adminKey) throws Exception{
		final String TAG = "DoodleRESTService#editPoll";
		try {
			makeOAuth2LeggedHandshake();
			//create new POST request
			HttpPut request = new HttpPut("http://doodle.com/api1/polls/" + pollID);
			//construct the xml payload
			String payload = XMLUtil.serializePoll(poll);
			request.setEntity(new StringEntity(payload));
			request.setHeader("Content-Type", "application/xml");
			//add admin key as request http header
			request.addHeader("X-DoodleKey", adminKey);
			//sign the request
			oauthConsumer.sign(request);
			//execute the request
			HttpResponse response = client.execute(request);
			StatusLine status = response.getStatusLine();
			//we expect 200 OK
			switch (status.getStatusCode()) {
			case HttpStatus.SC_OK: break;
			case HttpStatus.SC_UNAUTHORIZED: throw new Exception("the keys do not match");
			//unexpected response code
			default: {
				handleUnexpectedResponse(TAG, response);
				throw new Exception("Invalid response code: " + status.toString());}
			}
		} catch (OAuthException oaEx) {
			throw new Exception("Authentication error occurred", oaEx);
		} catch (IOException ioEx) {
			throw new Exception("Communication error occurred", ioEx);
		}
	}
	
	/**
	 * This method deletes a poll from doodle server given it's 16 characters identifier
	 * @param pollID 16-characters poll identifier
	 * @param adminKey 8-characters administrator key
	 * @throws Exception In the following cases exception is thrown<list>
	 * <li>When a poll with the passed identifier does not exist or is deleted</li>
	 * <li>When the client cannot be authenticated with the server</li>
	 * <li>When communication error occurred</li>
	 * </list>
	 */
	public boolean deletePollByID(String pollID, String adminKey) throws Exception{
		final String TAG = "DoodleRESTService#deletePollByID";
		try {
			//make oauth handshake to authorize consumer
			makeOAuth2LeggedHandshake();
			//create new DELETE request
			HttpDelete request = new HttpDelete("http://doodle.com/api1/polls/" + pollID);
			request.setHeader("Content-Type", "application/xml");
			//add admin key as request http header
			request.addHeader("X-DoodleKey", adminKey);
			//sign the request
			oauthConsumer.sign(request);
			//execute the request
			HttpResponse response = client.execute(request);
			StatusLine status = response.getStatusLine();
			//we expect 204 NO CONTENT
			switch (status.getStatusCode()) {
			case HttpStatus.SC_NO_CONTENT: return true;
			case HttpStatus.SC_UNAUTHORIZED: throw new Exception("the keys do not match");
			//unexpected response code
			default: {
				handleUnexpectedResponse(TAG, response);
				throw new Exception("Invalid response code: " + status.toString());}
			}
		} catch (OAuthException oaEx) {
			throw new Exception("Authentication error occurred", oaEx);
		} catch (IOException ioEx) {
			throw new Exception("Communication error occurred", ioEx);
		}
	}
	
	/**
	 * After this method is executed the oauthConsumer will be authorized and can sign the subsequent request
	 * @throws OAuthException
	 */
	private void makeOAuth2LeggedHandshake() throws OAuthException {
		//obtain Authorization URL
		String authUrl = oauthProvider.retrieveRequestToken(oauthConsumer, OAuth.OUT_OF_BAND);
		//the authorization url should be used to redirect the user to the service provider where he/she
		//will enter user and password and thus will authorize the request token.
		//After authorization the user will be shown a verification ID, which must be used as
		//oauth_verifier parameter. This is required in OAuth 1.0a
		//However in doodle 2-legged case we don't need this verification id
		//and we use null
		oauthProvider.retrieveAccessToken(oauthConsumer, null);
	}

	private void handleUnexpectedResponse(String TAG, HttpResponse response) {
		HttpEntity entity = response.getEntity();
		try {
			if (entity != null) {
				BufferedReader reader = null;

				reader = new BufferedReader(new InputStreamReader(entity.getContent()));

				StringBuilder builder = new StringBuilder();
				String line;

				while ((line = reader.readLine()) != null) {
					builder.append(line).append(System.getProperty("line.separator"));
				}
				Log.e(TAG, builder.toString());
			}
		} catch (IOException ioEx) {
			Log.e(TAG, "Error while reading response content", ioEx);
		}
	}
}
